/*
 * Copyright 2011-2012 Redia A/S
 * 
 * This file is part of Redias Library App for Android.
 *
 *  Redia Library App for Android is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Redia Library App for Android is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *  
 *  You should have received a copy of the GNU General Public License
 *  along with Redia Library App for Android.  If not, see <http://www.gnu.org/licenses/>.
 */
package dk.redia.libraryapp.net;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;

import dk.redia.lib.log.Log;

/**
 * Abstract class for managing large data set backed by a
 * remote data store
 * 
 * @author boll
 *
 */
public abstract class RemoteListManager<T> {
	
	private static final int chunkSize = 20;
	
	public static final int STATE_NOT_LOADED = 0;
	public static final int STATE_LOADING = 1;
	public static final int STATE_LOADED = 2;
	
	private static final String logTag = "RemoteListManager";
	
	private List<RemoteListManagerListener> listeners;
	// Stores any extra data collected by the calls to loadChunk
	// Eg. searches may return various facet counts etc. These could
	// be stored here
	private Map<String, Object> extraResultData;
	private Vector<T> data;
	private Vector<Integer> dataStates;
	private Map<String, Object> params; // Store params for use when loading data in sub classes
	
	public RemoteListManager() {
		data = new Vector<T>();
		dataStates = new Vector<Integer>();
		listeners = new ArrayList<RemoteListManagerListener>();
		params = new HashMap<String, Object>();
		extraResultData = new HashMap<String, Object>();
	}
	
	public void setExtraResultData(String key, Object value) {
		this.extraResultData.put(key, value);
	}
	
	public Object getExtraResultData(String key) {
		return this.extraResultData.get(key);
	}
	
	public void setParam(String key, Object value) {
		params.put(key, value);
	}
	
	public void unsetParam(String key) {
		params.remove(key);
	}
	
	public Object getParam(String key) {
		return params.get(key);
	}
	
	public void addListener(RemoteListManagerListener l) {
		listeners.add(l);
	}
	
	public void removeListener(RemoteListManagerListener l) {
		listeners.remove(l);
	}
	
	private void notifyListenersLoadedStateChanged(List<Integer> indices, int state) {
		List<RemoteListManagerListener> listenersCp = new ArrayList<RemoteListManagerListener>(listeners);
		for (RemoteListManagerListener remoteListManagerListener : listenersCp) {
			remoteListManagerListener.loadedStateChanged(indices, state);
		}
	}
	
	private void notifyListenersListLengthChanged(int newSize) {
		List<RemoteListManagerListener> listenersCp = new ArrayList<RemoteListManagerListener>(listeners);
		for (RemoteListManagerListener remoteListManagerListener : listenersCp) {
			remoteListManagerListener.listLengthChanged(newSize);
		}
	}
	
	private void notifyListenersChunkLoaded(int offset, int size) {
		List<RemoteListManagerListener> listenersCp = new ArrayList<RemoteListManagerListener>(listeners);
		for (RemoteListManagerListener remoteListManagerListener : listenersCp) {
			remoteListManagerListener.chunkLoaded(offset, size);
		}
	}
	
	private void notifyListenersChunkLoadFailed(int offset, int size, String message) {
		List<RemoteListManagerListener> listenersCp = new ArrayList<RemoteListManagerListener>(listeners);
		for (RemoteListManagerListener remoteListManagerListener : listenersCp) {
			remoteListManagerListener.chunkLoadFailed(offset, size, message);
		}
	}
	
	private void updateDataSize(int newSize) {
		if (data.size()==newSize) {
			return;
		}
		data.setSize(newSize);
		dataStates.setSize(newSize);
		notifyListenersListLengthChanged(newSize);
	}
	
	protected void chunkLoadFailed(int offset, int count, String message) {
		for (int i=0;i<count;i++) {
			int idx = i + offset;
			if (idx<dataStates.size()) {
				dataStates.set(idx, STATE_NOT_LOADED);
			} else {
				break;
			}
		}
		notifyListenersChunkLoadFailed(offset, count, message);
	}
	
	protected void chunkLoaded(int offset, int listSize, List<T> chunk) {
		Log.d(RemoteListManager.logTag, "Loaded chunk with " + chunk.size() + " items at offset " + offset + ". Total result count is " +  listSize);
		if (offset+chunk.size()>listSize) {
			throw new RuntimeException("Specified listSize" + listSize + " is not enough to hold " + chunk.size() + " objects from offset " + offset);
		}
		updateDataSize(listSize);
		List<Integer> loadedIndices = new ArrayList<Integer>();
		for (int i=0;i<chunk.size();i++) {
			int idx = i + offset;
			Log.d(RemoteListManager.logTag, "Updating data item at index " + idx);
			T d = chunk.get(i);
			data.set(idx, d);
			loadedIndices.add(idx);
		}
		for (int i=0;i<chunk.size();i++) {
			int idx = i + offset;
			dataStates.set(idx, STATE_LOADED);
		}
		notifyListenersChunkLoaded(offset, chunk.size());
		notifyListenersLoadedStateChanged(loadedIndices, STATE_LOADED);
	}
	
	public void init() {
		prepareLoadListChunk(0, RemoteListManager.chunkSize);
	}
	
	public int getLoadState(int index) {
		if (index<dataStates.size()) {
			Integer dataState = dataStates.get(index);
			if (dataState==null) {
				return STATE_NOT_LOADED;
			} else {
				return dataState;
			}
		} else {
			Log.e(logTag, "Attempt to get load state at index " + index + " but dataStates list only has length " + dataStates.size());
			return STATE_NOT_LOADED;
		}
	}
	
	public int getSize() {
		return data.size();
	}
	
	public T getItem(int index) {
		return data.get(index);
	}
	
	public void ensureIndexLoad(int index) {
		Integer dataState = STATE_NOT_LOADED;
		if (dataStates.size()>index) {
			 dataState = dataStates.get(index);	
		} else {
			Log.w(logTag, "Ensuring that index " + index + " is loaded. But dataStates only has size " +  dataStates.size());
		}
		if (dataState==null || dataState==STATE_NOT_LOADED) {
			int chunkOffset = (index/RemoteListManager.chunkSize)*RemoteListManager.chunkSize;
			Log.d(RemoteListManager.logTag, "Calculated offset to " + chunkOffset + " for index " + index);
			prepareLoadListChunk(chunkOffset, RemoteListManager.chunkSize);
		} else if (dataState==STATE_LOADING) {
			return;
		} else if (dataState==STATE_LOADED) {
			return;
		} else {
			Log.e(RemoteListManager.logTag, "Unknown data state: " + dataState + " at index " + index);
		}
	}
	
	
	public void reset() {
		updateDataSize(0);
		prepareLoadListChunk(0, RemoteListManager.chunkSize);
	}
	
	private void prepareLoadListChunk(int offset, int count) {
		for (int i=0;i<count;i++) {
			int idx = offset + i;
			if (idx>=dataStates.size()) {
				break;
			}
			dataStates.set(idx, STATE_LOADING);
		}
		loadListChunk(offset, count);
	}
	
	public abstract void loadListChunk(int offset, int count);
}
